﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace Transformable_Engine_v2.Engine.Network
{
    /// <summary>
    /// Utility class to serialize and deserialize data into byte arrays
    /// </summary>
    public static class Serialization
    {
        private static Encoding _encoding = Encoding.UTF8;

        /// <summary>
        /// The encoding use to serialize strings
        /// </summary>
        public static Encoding Encoding
        {
            get { return _encoding; }
            set { _encoding = value; }
        }

        /// <summary>
        /// Method that serializes an object into a byte array, only properties will be serialized
        /// </summary>
        /// <typeparam name="T">The type of the object</typeparam>
        /// <param name="obj">The object</param>
        /// <returns>The serialized object</returns>
        public static byte[] Serialize<T>(T obj) where T : class
        {
            var stream = new MemoryStream();
            stream.SerializeValue(obj);
            return stream.ToArray();

        }

        private static void Serialize(Type objType, object obj, MemoryStream stream)
        {
            foreach (var property in objType.GetProperties())
            {
                var value = property.GetValue(obj, null);
                stream.SerializeValue(value);
            }
        }

        private static void SerializeValue(this MemoryStream stream, object obj)
        {
            if (obj.GetType().IsPrimitive)
            {
                byte[] buffer = null;
                
                if (obj is byte)
                    buffer = new[] { (byte)obj };
                else if (obj is bool)
                    buffer = BitConverter.GetBytes((bool)obj);
                else if (obj is char)
                    buffer = BitConverter.GetBytes((char)obj);
                else if (obj is double)
                    buffer = BitConverter.GetBytes((double)obj);
                else if (obj is float)
                    buffer = BitConverter.GetBytes((float)obj);
                else if (obj is int)
                    buffer = BitConverter.GetBytes((int)obj);
                else if (obj is long)
                    buffer = BitConverter.GetBytes((long)obj);
                else if (obj is short)
                    buffer = BitConverter.GetBytes((short)obj);
                else if (obj is uint)
                    buffer = BitConverter.GetBytes((uint)obj);
                else if (obj is ulong)
                    buffer = BitConverter.GetBytes((ulong)obj);
                else if (obj is ushort)
                    buffer = BitConverter.GetBytes((ushort)obj);

                if (buffer != null) 
                    stream.Write(buffer, 0, buffer.Length);
            }
            else if(obj is Enum) {
                byte[] buffer = BitConverter.GetBytes((int)obj);
                stream.Write(buffer, 0, buffer.Length);
            }
            else if ((obj as String) != null)
            {
                var str = (String) obj;
                stream.SerializeValue(str.Length);
                byte[] buffer = GetBytes(str);
                stream.Write(buffer, 0, buffer.Length);
            }
            else if (obj.GetType().IsArray)
            {
                var array = (Array) obj;
                stream.SerializeValue(array.Length);

                foreach (var value in array)
                {
                    stream.SerializeValue(value);
                }
            }
            else if ((obj as IList) != null)
            {
                var list = (IList) obj;
                stream.SerializeValue(list.Count);

                foreach (var value in list)
                {
                    stream.SerializeValue(value);
                }
            }
            else
            {
                Serialize(obj.GetType(), obj, stream);
            }
        }

        private static byte[] GetBytes(String value)
        {
            return Encoding.GetBytes(value);
        }

        /// <summary>
        /// Method that deserializes a byte array into an object
        /// </summary>
        /// <typeparam name="T">The type that needs to be deserialized</typeparam>
        /// <param name="data">The data that will be deserialized into T</param>
        /// <returns>The deserialized object of type T</returns>
        public static T Deserialize<T>(byte[] data) where T : class, new()
        {
            var stream = new MemoryStream(data);
            return stream.DeserializeValue(typeof(T)) as T;
        }

        private static void Deserialize(Type objType, object obj, MemoryStream stream)
        {
            foreach (var property in objType.GetProperties())
            {
                var value = stream.DeserializeValue(property.PropertyType);
                property.SetValue(obj, value, null);
            }
        }

        private static object DeserializeValue(this MemoryStream stream, Type type)
        {
            if (type.IsPrimitive)
            {
                var buffer = new byte[4];
                
                if (type == typeof (byte))
                {
                    return stream.ReadByte();
                }
                if (type == typeof (bool))
                {
                    stream.Read(buffer, 0, sizeof(bool));
                    return BitConverter.ToBoolean(buffer, 0);
                }
                if (type == typeof(char))
                {
                    stream.Read(buffer, 0, sizeof(char));
                    return BitConverter.ToChar(buffer, 0);
                }
                if (type == typeof(double))
                {
                    stream.Read(buffer, 0, sizeof(double));
                    return BitConverter.ToDouble(buffer, 0);
                }
                if (type == typeof(float))
                {
                    stream.Read(buffer, 0, sizeof(float));
                    return BitConverter.ToSingle(buffer, 0);
                }
                if (type == typeof(int))
                {
                    stream.Read(buffer, 0, sizeof(int));
                    return BitConverter.ToInt32(buffer, 0);
                }
                if (type == typeof(long))
                {
                    stream.Read(buffer, 0, sizeof(long));
                    return BitConverter.ToInt64(buffer, 0);
                }
                if (type == typeof(short))
                {
                    stream.Read(buffer, 0, sizeof(short));
                    return BitConverter.ToInt16(buffer, 0);
                }
                if (type == typeof(uint))
                {
                    stream.Read(buffer, 0, sizeof(uint));
                    return BitConverter.ToUInt32(buffer, 0);
                }
                if (type == typeof(ulong))
                {
                    stream.Read(buffer, 0, sizeof(ulong));
                    return BitConverter.ToUInt64(buffer, 0);
                }
                if (type == typeof(ushort))
                {
                    stream.Read(buffer, 0, sizeof(ushort));
                    return BitConverter.ToUInt16(buffer, 0);
                }
            }
            else if (type.BaseType == typeof(Enum)) 
            {
                var buffer = new byte[4];
                stream.Read(buffer, 0, sizeof(int));
                return BitConverter.ToInt32(buffer, 0);
            }
            else if (type == typeof(String))
            {
                var length = (int)stream.DeserializeValue(typeof(int));
                return GetString(stream, length);
            }
            else if (type.IsArray)
            {
                var length = (int)stream.DeserializeValue(typeof(int));
                var array = Array.CreateInstance(type.GetElementType(), length);

                for (var i = 0; i < length; i++)
                {
                    var value = stream.DeserializeValue(type.GetElementType());

                    if(type == typeof(byte[])) {
                        array.SetValue(Convert.ToByte(value), i);
                    }
                    else
                        array.SetValue(value, i); 
                }
                return array;
            }
            else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
            {
                var length = (int)stream.DeserializeValue(typeof(int));
                var list = (IList)Activator.CreateInstance(type);

                for (var i = 0; i < length; i++)
                {
                    var value = stream.DeserializeValue(type.GetGenericArguments()[0]);
                    list.Add(value);
                }
                return list;
            }
            else
            {
                object obj = Activator.CreateInstance(type);
                Deserialize(type, obj, stream);
                return obj;
            }
            return null;
        }

        private static String GetString(MemoryStream stream, int length)
        {
            var buffer = new byte[length];
            stream.Read(buffer, 0, length);
            return Encoding.GetString(buffer, 0, length);
        }
    }
}
